package com.tyylab.kestrel.framework.spring.rpc.feign.config;

import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.tyylab.kestrel.framework.core.annoutil.SpringCoreAnnotationUtil;
import com.tyylab.kestrel.framework.core.build.DefaultClassBuilder;
import com.tyylab.kestrel.framework.core.build.IClassBuilder;
import com.tyylab.kestrel.framework.core.util.ReflectionUtil;
import com.tyylab.kestrel.framework.spring.rpc.feign.annotation.EnableKestrelFeignClient;
import com.tyylab.kestrel.framework.spring.rpc.feign.annoutil.SpringFeignAnnotationUtil;
import com.tyylab.kestrel.framework.spring.rpc.feign.impl.FeignApiGenerator;
import com.tyylab.kestrel.framework.spring.rpc.feign.impl.FeignClientConfigGenerator;
import com.tyylab.kestrel.framework.spring.rpc.feign.impl.FeignFallbackGenerator;
import com.tyylab.kestrel.framework.spring.rpc.feign.impl.ImplWithFeignGenerator;

import cn.hutool.core.util.StrUtil;
import lombok.Builder;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.description.annotation.AnnotationDescription;

/**
 * Feign Client Registrar with fallback support
 * @author tyylab
 */
@Slf4j
@Configuration
public class KestrelFeignClientRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // get all package
        var annotationAttributes = importingClassMetadata.getAnnotationAttributes(EnableKestrelFeignClient.List.class.getName());
        var annotationAttribute = importingClassMetadata.getAnnotationAttributes(EnableKestrelFeignClient.class.getName());
        Map<String, Set<String>> packageMap= Maps.newHashMap();
        // multi annotate
        if (null != annotationAttributes) {
            AnnotationAttributes[] as = (AnnotationAttributes[])((AnnotationAttributes) annotationAttributes).get("value");
            if (null != as) {
                for (AnnotationAttributes attributes : as) {
                    String serviceName = (String) attributes.get("serviceName");
                    String[] apiBasePackages = (String[]) attributes.get("apiBasePackages");
                    if (!packageMap.containsKey(serviceName)) {
                        Set<String> list = new HashSet<>();
                        packageMap.put(serviceName, list);
                    }
                    for (String s : apiBasePackages) {
                        packageMap.get(serviceName).add(s);
                    }
                }
            }
        }
        // single annotate
        if (null != annotationAttribute){
            String serviceName = (String) annotationAttribute.get("serviceName");
            String[] apiBasePackages = (String[]) annotationAttribute.get("apiBasePackages");
            if (!packageMap.containsKey(serviceName)) {
                Set<String> list = new HashSet<>();
                packageMap.put(serviceName, list);
            }
            for (String s : apiBasePackages) {
                packageMap.get(serviceName).add(s);
            }
        }

        // generate configuration
        FeignClientConfigGenerator feignClientConfigGenerator = new FeignClientConfigGenerator(packageMap);
        FeignApiGenerator feignApiGenerator = new FeignApiGenerator(feignClientConfigGenerator);
        ImplWithFeignGenerator implWithFeignGenerator = new ImplWithFeignGenerator();
        FeignFallbackGenerator fallbackGenerator = new FeignFallbackGenerator();

        Set<String> exists = new HashSet<>();
        Set<String> packageSet = new HashSet<>();
        Set<Class> feignInterfaces = new HashSet<>();
        List<FeignImplMeta> feignImplMetas = Lists.newLinkedList();
        
        for (Map.Entry<String, Set<String>> entry : packageMap.entrySet()) {
            for (String basePackage : entry.getValue()) {
                List<Class<?>> interfaces = ReflectionUtil.loadInterfaces(basePackage);
                if (null != interfaces && interfaces.size() > 0) {
                    for (Class<?> aClass : interfaces) {
                        if (exists.contains(aClass.getName())) {
                            continue;
                        }
                        // generate fallback class
                        Class<?> fallbackClass = fallbackGenerator.generateFallback(aClass);
                        //define feign api
                        Class<?> feignApi = feignApiGenerator.generateFeignApi(aClass, fallbackClass);
                        packageSet.add(feignApi.getPackageName());
                        feignImplMetas.add(FeignImplMeta.builder().feignApi(feignApi).aClass(aClass).build());

                        feignInterfaces.add(feignApi);
                        exists.add(aClass.getName());
                        
                        // Register fallback class
                        
                        if (fallbackClass != null) {
                            BeanDefinition fallbackBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(fallbackClass).getBeanDefinition();
                            fallbackBeanDefinition.setAutowireCandidate(true);
                            registry.registerBeanDefinition(fallbackClass.getSimpleName(), fallbackBeanDefinition);
                            log.debug("Registered fallback bean: {}", fallbackClass.getSimpleName());
                        }
                    }
                }
            }
        }

        exists.clear();
        Class[] clientClasses = feignInterfaces.size()>0 ? feignInterfaces.toArray(new Class[feignInterfaces.size()])
                :new Class[0];

        // regist configuration
        Class<?> configClz = generateFeignConfigClz(packageSet, clientClasses);
        BeanDefinition cbeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(configClz).getBeanDefinition();
        cbeanDefinition.setAutowireCandidate(true);
        registry.registerBeanDefinition(configClz.getSimpleName(), cbeanDefinition);

        for (FeignImplMeta feignImplMeta : feignImplMetas) {
            //define impl with feign
            Class<?> implWithFeign = implWithFeignGenerator.generate(feignImplMeta.getAClass(), feignImplMeta.getFeignApi());
            BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(implWithFeign).getBeanDefinition();
            beanDefinition.setAutowireCandidate(true);
            registry.registerBeanDefinition(feignImplMeta.getAClass().getSimpleName(), beanDefinition);
        }

        feignImplMetas.clear();
    }

    @Data
    @Builder
    static class FeignImplMeta{
        Class<?> feignApi;
        Class<?> aClass;
    }

    Class generateFeignConfigClz(Set<String> packageList,Class[] clientClasses){
        String clzName = "FeignClientConfig";
        IClassBuilder classBuilder = new DefaultClassBuilder();
        AnnotationDescription annotationDescription =
                SpringFeignAnnotationUtil.enableFeign( packageList.toArray(new String[packageList.size()]), null, null, clientClasses);
        AnnotationDescription configAnno =
                SpringCoreAnnotationUtil.configuration(StrUtil.lowerFirst(clzName));
        return classBuilder.defineClass()
                .clazz(clzName)
                .modifiers(Modifier.PUBLIC)
                .annotate(annotationDescription, configAnno)
                .end()
                .build();
    }
}
